<?php
/**
 * LICENSE
 *
 * Copyright (C) 2010 Nicholas Miller
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or any
 * later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * PayPal SOAP Client
 *
 * @author     Nicholas Miller <nicholas.j.miller@gmail.com>
 * @copyright  2010 Nicholas Miller
 * @category   PaypalSoap
 */
class PayPalSoap_Client extends Zend_Soap_Client implements SplSubject
{
    /**
     * @var string
     */
    const ACK_FAILURE = 'Failure';

    /**
     * @var string
     */
    const ACK_SUCCESS = 'Success';

    /**
     * @var PayPalSoap_Endpoint
     */
    protected $_endpoint;

    /**
     * Attached observers
     * @var array of SplObserver
     */
    protected $_observers = array();

    /**
     * The last object returned from a SOAP call
     * @var mixed
     */
    protected $_lastResult = null;

    /**
     * Default observers to be attached at instantiation
     * @var array of SplObserver
     */
    protected static $_defaultObservers = array();

    /**
     * Class Constructor
     * @param PayPalSoap_Credentials $credentials
     * @param PayPalSoap_Endpoind $endpoint
     */
    public function  __construct(PayPalSoap_Credentials $credentials, PayPalSoap_Endpoint $endpoint)
    {
        parent::__construct(
            dirname(__FILE__) . '/Support/PayPalSvc.wsdl',
            array('soap_version' => SOAP_1_1)
        );

        $auth = new stdClass();
        $auth->Credentials->Username  = $credentials->getUsername();
        $auth->Credentials->Password  = $credentials->getPassword();
        $auth->Credentials->Signature = $credentials->getSignature();
        $auth->Credentials->Subject   = $credentials->getSubject();

        $this->addSoapInputHeader(
            new SoapHeader('urn:ebay:api:PayPalAPI', 'RequesterCredentials', $auth)
        );


        $this->_endpoint = $endpoint;
        $this->setUri((string)$endpoint);

        if (!empty(self::$_defaultObservers)) {
            foreach (self::$_defaultObservers as $observer) {
                /* @var $observer SplObserver */
                $this->attach($observer);
            }
        }
    }

    /**
     * Refunds a Transaction
     *
     * @param PayPalSoap_Type_RefundTransactionRequest $request
     * @param PayPalSoap_Type_RefundTransactionResponse
     */
    public function RefundTransaction(PayPalSoap_Type_RefundTransactionRequest $request)
    {
        $data = new stdClass();
        $data->RefundTransactionRequest = $request;
        
        return $this->__call('RefundTransaction', array($data));
    }

    /**
     * Requests an onboarding token
     * 
     * @param PayPalSoap_Type_EnterBoardingRequest $request
     * @return PayPalSoap_Type_EnterBoardingResponse
     */
    public function EnterBoarding(PayPalSoap_Type_EnterBoardingRequest $request)
    {
        $data = new stdClass();
        $data->EnterBoardingRequest = $request;

        return $this->__call('EnterBoarding', array($data));
    }

    /**
     * Starts an Access Permission Request
     * @return PayPalSoap_Type_SetAccessPermissionsResponse
     */
    public function SetAccessPermissions(PayPalSoap_Type_SetAccessPermissionsRequest $request)
    {
        $data = new stdClass();
        $data->SetAccessPermissionsRequest = $request;

        return $this->__call('SetAccessPermissions', array($data));
    }

    /**
     * Gets details regarding a PayPal Permissions API request
     * @param PayPalSoap_Type_GetAccessPermissionDetailsRequest $request
     * @return PayPalSoap_Type_GetAccessPermissionDetailsResponse
     */
    function GetAccessPermissionDetails(PayPalSoap_Type_GetAccessPermissionDetailsRequest $request)
    {
        $data = new stdClass();
        $data->GetAccessPermissionDetailsRequest = $request;

        return $this->__call('GetAccessPermissionDetails', array($data));
    }

    /**
     * Hook to process results from SOAP call
     * Checks for error feedback from paypal, and places data
     * in the correct class type
     *
     * @param stdClass $result Result of SOAP execution
     * @throws PayPalSoap_Exception If the PayPal response message is an error
     * @return mixed
     */
    protected function  _preProcessResult($result)
    {
        $result = parent::_preProcessResult($result);

        if (!empty($result->Ack) && strcasecmp($result->Ack, self::ACK_FAILURE) == 0) {
            $msg = sprintf('%s (%s)', $result->Errors->ShortMessage, $result->Errors->LongMessage);
            $code = $result->Errors->ErrorCode;
            throw new PayPalSoap_Exception($msg, $code);
        }
        
        $type = $this->_extractReturnType($this->getLastMethod());
        $return = new $type;

        $reflector = new ReflectionClass($type);
        foreach ($reflector->getProperties() as $property) {
            /* @var $property ReflectionProperty */
            $tmp = $property->getName();
            $return->$tmp = empty($result->$tmp)
                    ? null : $result->$tmp;
        }

        $this->_lastResult = $return;

        // Fire hooks for the any attached observers
        $this->notify();

        return $return;
    }

    /**
     * Extracts the return type from a given method
     * @param string $method
     * @return string Return type
     */
    protected function _extractReturnType($method)
    {
        // in order to keep down on reflection,
        // we're going to keep a list of items we've seen.
        static $seen = array();

        if (isset($seen[$method])) {
            return $seen[$method];
        }

        $regex = '(@return\s+([:.\w\\\]+)(?:[\t ]+(\S*))?(?:[\t ]+(\S*))?\s*$)m';

        $reflector  = new ReflectionClass(get_class($this));
        $method     = $reflector->getMethod($method);

        if (!preg_match($regex, $method->getDocComment(), $regs)) {
            throw new RuntimeException(
                'The supplied method "' . $method . '" does not provide a ' .
                'doc block with a return type'
            );
        }

        return $regs[1];
    }

    /**
     * Notifies any attached observers
     * (Defined by SplSubject interface)
     * @return void
     */
    public function notify()
    {
        foreach ($this->_observers as $observer) {
            /* @var $observer  PayPalSoap_Client_Observer_Abstract */
            $observer->update($this);
        }
    }

    /**
     * Attaches an Observer
     * (Defined by SplSubject interface)
     * @param  SplObserver $observer
     * @throws InvalidArgumentException If the SplObserver does not extend
     *                                  PayPalSoap_Client_Observer_Abstract
     * @return void
     */
    public function attach(SplObserver $observer)
    {
        if (!($observer instanceof PayPalSoap_Client_Observer_Abstract)) {
            throw new InvalidArgumentException(
                'The provided observer must be an instance of PayPalSoap_Client_Observer_Abstract. ' .
                '("' . get_class($observer) . '") provided'
            );
        }

        $this->_observers[] = $observer;
    }

    /**
     * Detaches an Observer
     * (Defined by SplSubject interface)
     * @param  SplObserver $observer
     * @throws InvalidArgumentException If the SplObserver does not extend
     *                                  PayPalSoap_Client_Observer_Abstract
     * @return void
     */
    public function detach(SplObserver $observer)
    {
        if (!($observer instanceof PayPalSoap_Client_Observer_Abstract)) {
            throw new InvalidArgumentException(
                'The provided observer must be an instance of PayPalSoap_Client_Observer_Abstract. ' .
                '("' . get_class($observer) . '") provided'
            );
        }

        $pos = null;
        if (false !== ($pos = array_search($observer, $this->_observers))) {
            unset($this->_observers[$pos]);
        }
    }

    /**
     * Returns the array of attached observers
     * @return array of PayPalSoap_Client_Observer_Abstract
     */
    public function getObservers()
    {
        return $this->_observers;
    }

    /**
     * Returns the last object generated from a SOAP call
     * @return mixed
     */
    public function getLastResult()
    {
        return $this->_lastResult;
    }

    /**
     * Registers an observer to be attached anytime the SoapClient is instantiated
     *
     * @param $observer Array of SplObserver objects
     * @return void
     */
    public static function registerDefaultObserver(SplSubject $observer)
    {
        self::$_defaultObservers[] = $observer;
    }

    /**
     * Registers an observer to be attached anytime the SoapClient is instantiated
     *
     * @param $observer Array of SplObserver objects
     * @return void
     */
    public static function unregisterDefaultObserver(SplSubject $observer)
    {
        $position = null;
        if (false === ($position = array_search($observer, self::$_defaultObservers))) {
            return;
        }

        unset(self::$_defaultObservers[$position]);
    }

    /**
     * Clears all registered default observers
     * 
     * @return void
     */
    public static function clearDefaultObservers()
    {
        self::$_defaultObservers = array();
    }
}


